package pers.mor.tomcat.catalina;

import cn.hutool.core.date.TimeInterval;
import cn.hutool.core.util.StrUtil;
import cn.hutool.log.LogFactory;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import pers.mor.tomcat.classloader.CommonClassLoader;
import pers.mor.tomcat.classloader.WebappClassLoader;
import pers.mor.tomcat.config.ContextXML;
import pers.mor.tomcat.exception.WebConfigDuplicatedException;
import pers.mor.tomcat.filter.StandardFilterConfig;
import pers.mor.tomcat.http.ApplicationContext;
import pers.mor.tomcat.http.SessionManager;
import pers.mor.tomcat.http.StandardServletConfig;
import pers.mor.tomcat.listener.ContextEvent;
import pers.mor.tomcat.watcher.ContextFileChangeWatcher;

import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpSessionListener;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

public class Context {

    private String path;
    private String base;
    private Host host;
    private boolean reloadable = false;
    private File contextWebXmlFile;
    private ServletContext servletContext;
    private Map<Class<?>, HttpServlet> servletPool;
    private Map<String, Filter> filterPool;

    private Map<String, String> url_servletName;
    private Map<String, String> url_servletClassName;
    private Map<String, String> servletName_className;
    private Map<String, String> className_servletName;
    private Map<String, Map<String, String>> servletClassName_initParams;
    private List<String> loadOnStartupServletClassNames;

    private Map<String, List<String>> url_filterClassName;
    private Map<String, List<String>> url_FilterNames;
    private Map<String, String> filterName_className;
    private Map<String, String> className_filterName;
    private Map<String, Map<String, String>> filter_className_init_params;

    private List<ServletContextListener> listeners;

    private WebappClassLoader webappClassLoader;

    private ContextFileChangeWatcher contextFileChangeWatcher;

    public Context(String path, String base, Host host, boolean reloadable) {
        this.path = path;
        this.base = base;
        this.host = host;
        this.reloadable = reloadable;
        this.contextWebXmlFile = new File(base, ContextXML.getWebInf());
        this.servletContext = new ApplicationContext(this);
        this.servletPool = new HashMap<>();
        this.filterPool = new HashMap<>();

        this.url_servletName = new HashMap<>();
        this.url_servletClassName = new HashMap<>();
        this.servletName_className = new HashMap<>();
        this.className_servletName = new HashMap<>();
        this.servletClassName_initParams = new HashMap<>();
        this.loadOnStartupServletClassNames = new ArrayList<>();

        this.url_filterClassName = new HashMap<>();
        this.url_FilterNames = new HashMap<>();
        this.filterName_className = new HashMap<>();
        this.className_filterName = new HashMap<>();
        this.filter_className_init_params = new HashMap<>();

        this.listeners = new ArrayList<>();

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        this.webappClassLoader = new WebappClassLoader(this.base, (CommonClassLoader)classLoader);

        deploy();
    }

    private void deploy() {
        TimeInterval interval = new TimeInterval();
        interval.start();
        LogFactory.get().info("正在部署 webapp : 目录 {}",this.base);
        this.init();
        LogFactory.get().info("目录：{} 的 webapp 应用程序部署已经在{}ms中完成", this.base, interval.intervalMs());
        if(this.reloadable == true){
            this.contextFileChangeWatcher = new ContextFileChangeWatcher(this);
            this.contextFileChangeWatcher.start();
        }
    }

    private void init() {
        if (!this.contextWebXmlFile.exists()) {
            LogFactory.get().info("发现 {} 文件缺失", this.contextWebXmlFile);
            return;
        }
        try {
            this.checkDuplicated();
        } catch (WebConfigDuplicatedException e) {
            LogFactory.get().error(e);
            return;
        }

        this.parseServletMapping();
        this.parseServletInitParams();
        this.parseLoadOnStartup();
        this.handleLoadOnStartup();

        this.parseFilterMapping();
        this.parseFilterInitParams();
        this.initFilter();

        this.parseListener();

        this.triggerListener("init");
    }

    private void parseServletMapping() {
        Element root = this.parseWebXml();
        List<Element> servletTag = root.elements("servlet");
        List<Element> servletMappingTag = root.elements("servlet-mapping");

        this.extract(servletTag, this.servletName_className,"servlet-name","servlet-class");
        this.extract(servletTag, this.className_servletName,"servlet-class","servlet-name");
        this.extract(servletMappingTag, this.url_servletName,"url-pattern","servlet-name");
        this.extract(servletMappingTag, this.url_servletClassName,"url-pattern","servlet-class");
    }

    private void parseServletInitParams() {
        this.initParams("servlet", this.servletClassName_initParams);
    }

    private void parseLoadOnStartup() {
        Element root = this.parseWebXml();
        List<Element> servletTags = root.elements("servlet");
        for (Element servletTag : servletTags) {
            String servletClassName = servletTag.elementText("servlet-class");
            String loadOnStartup = servletTag.elementText("load-on-startup");
            if (loadOnStartup != null && !loadOnStartup.equals("")) {
                this.loadOnStartupServletClassNames.add(servletClassName);
            }
        }
    }

    private void handleLoadOnStartup() {
        for (String servletClassName : this.loadOnStartupServletClassNames) {
            try {
                Class<?> aClass = this.webappClassLoader.loadClass(servletClassName);
                this.getServlet(aClass);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ServletException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    private void initParams(String TagName,Map<String, Map<String, String>> initParamMap) {
        Element root = this.parseWebXml();
        List<Element> tags = root.elements(TagName);
        for (Element tag : tags) {
            List<Element> initParams = tag.elements("init-param");
            String ClassName = tag.elementText(TagName + "-class");
            Map<String, String> map = new HashMap<>();
            for (Element initParam : initParams) {
                String paramName = initParam.elementText("param-name");
                String paramValue = initParam.elementText("param-value");
                map.put(paramName, paramValue);
            }
            initParamMap.put(ClassName, map);
        }
    }

    private void parseFilterMapping() {
        Element root = this.parseWebXml();
        List<Element> filterTag = root.elements("filter");
        List<Element> filterMappingTag = root.elements("filter-mapping");

        this.extractFromFilterTag(filterTag, this.filterName_className,"filter-name","filter-class");
        this.extractFromFilterTag(filterTag, this.className_filterName,"filter-class","filter-name");
        this.extractFromFilterMapping(filterMappingTag, this.url_FilterNames,"url-pattern","filter-name");
        this.extractFromFilterMapping(filterMappingTag, this.url_filterClassName,"url-pattern","filter-class");
    }

    private void parseFilterInitParams() {
        this.initParams("filter", this.filter_className_init_params);
    }

    private void initFilter() {
        Set<String> classNames = this.className_filterName.keySet();
        for (String className : classNames) {
            try {
                Filter filter = this.filterPool.get(className);
                if (filter == null) {
                    Map<String, String> initParamMap = this.filter_className_init_params.get(className);
                    String filterName = this.className_filterName.get(className);

                    FilterConfig filterConfig = new StandardFilterConfig(this.servletContext, initParamMap, filterName);

                    Class<?> aClass = this.webappClassLoader.loadClass(className);
                    filter = (Filter)aClass.newInstance();
                    Method init = aClass.getMethod("init", FilterConfig.class);
                    init.invoke(filter, filterConfig);
                    this.filterPool.put(className, filter);
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

    }

    public List<Filter> getMatchedFilters(String requestPath) {
        List<Filter> filterList = new ArrayList<>();

        Set<String> matchedTestPathSet = new HashSet<>();
        Set<String> testPathSet = this.url_filterClassName.keySet();
        for (String testPath : testPathSet) {
            if (this.match(testPath, requestPath) == true) {
                matchedTestPathSet.add(testPath);
            }
        }

        Set<String> matchedFilterClassNameSet = new HashSet<>();
        for (String testPath : matchedTestPathSet) {
            List<String> classNameList = this.url_filterClassName.get(testPath);
            matchedFilterClassNameSet.addAll(classNameList);
        }

        for (String className : matchedFilterClassNameSet) {
            Filter filter = this.filterPool.get(className);
            filterList.add(filter);
        }

        return filterList;
    }

    private boolean match(String testPath, String requestPath) {
        // 以下的匹配规则是从tomcat源码中复制过来的。
        if (testPath == null) {
            return false;
        }

        // Case 1 - Exact Match
        if (testPath.equals(requestPath)) {
            return true;
        }

        // Case 2 - Path Match ("/.../*")
        if (testPath.equals("/*")) {
            return true;
        }
        // String.endsWith(String char)方法是判断某字符串是否以指定的后缀结尾
        if (testPath.endsWith("/*")) {
            // String.regionMatches检测两个字符串在一个区域内是否相等。
            // 第一个参数是testPath要对比的起始下标
            // 第二个参数是要比较的另一个字符串
            // 第三个和第四个参数是另一个字条串的起始下标和结束下标
            if (testPath.regionMatches(0, requestPath, 0, testPath.length() - 2)) {
                if (requestPath.length() == (testPath.length() - 2)) {
                    return true;
                } else if ('/' == requestPath.charAt(testPath.length() - 2)) {
                    return true;
                }
            }
            return false;
        }

        // Case 3 - Extension Match 用于匹配过虑.jsp或静态资源.jpg或.js或.css之类的
        if (testPath.startsWith("*.")) {
            int slash = requestPath.lastIndexOf('/');
            int period = requestPath.lastIndexOf('.');
            // (requestPath.length() - period) == (testPath.length() - 1
            // 这里有个规律：用字符串的长度减去.的下标，剩余长度如果一样，再比较他们的内容是否一样。
            if ((slash >= 0) && (period > slash) && (period != requestPath.length() - 1) && ((requestPath.length() - period) == (testPath.length() - 1))) {
                return (testPath.regionMatches(2, requestPath, period + 1, testPath.length() - 2));
            }
        }

        // Case 4 - "Default" Match
        return false; // NOTE - Not relevant for selecting filters
    }

    private void triggerListener(String action) {
        ContextEvent event = new ContextEvent(this.servletContext);
        for (ServletContextListener listener : this.listeners) {
            if (action.equals("init")) {
                listener.contextInitialized(event);
            } else if (action.equals("stop")) {
                listener.contextDestroyed(event);
            }
        }
    }

    private void addListener(ServletContextListener listener) {
        this.listeners.add(listener);
    }

    private void parseListener() {
        Element root = this.parseWebXml();
        List<Element> listenerTag = root.elements("listener");
        List<String> listenerClassNameList = new ArrayList<>();
        this.extract(listenerTag, listenerClassNameList, "listener-class");
        for (String className : listenerClassNameList) {
            try {
                Class<?> aClass = this.webappClassLoader.loadClass(className);
                Class<?>[] interfaces = aClass.getInterfaces();
                for (Class<?> anInterface : interfaces) {
                    String typeName = anInterface.getTypeName();
                    if (typeName.equals("javax.servlet.ServletContextListener")) {
                        ServletContextListener listener = (ServletContextListener)aClass.newInstance();
                        this.addListener(listener);
                    } else if (typeName.equals("javax.servlet.http.HttpSessionListener")) {
                        HttpSessionListener listener = (HttpSessionListener) aClass.newInstance();
                        SessionManager.addListener(this.path, listener);
                    }
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    private void checkDuplicated() throws WebConfigDuplicatedException {
        Element root = this.parseWebXml();
        List<Element> servletTag = root.elements("servlet");
        List<Element> servletMappingTag = root.elements("servlet-mapping");
        List<Element> filterTag = root.elements("filter");
        List<Element> listenerTag = root.elements("listener");

        String servletNameDuplicated = "servlet 名称重复,请保持其唯一性:{}";
        this.checkDuplicated(servletTag, "servlet-name", servletNameDuplicated);

        String classNameDuplicated = "servlet 类名重复,请保持其唯一性:{}";
        this.checkDuplicated(servletTag,"servlet-class", classNameDuplicated);

        String urlDuplicated = "servlet url 重复,请保持其唯一性:{}";
        this.checkDuplicated(servletMappingTag, "url-pattern", urlDuplicated);

        String filterNameDuplicated = "filter 名称重复,请保持其唯一性:{}";
        this.checkDuplicated(filterTag, "filter-name", filterNameDuplicated);

        String filterClassNameDuplicated = "filter 类名重复,请保持其唯一性:{}";
        this.checkDuplicated(filterTag,"filter-class", filterClassNameDuplicated);

        String listenerDuplicated = "listener 名称重复,请保持其唯一性:{}";
        this.checkDuplicated(listenerTag, "listener-class", listenerDuplicated);
    }

    private void checkDuplicated(List<Element> list, String regex, String errMsg) throws WebConfigDuplicatedException {

        ArrayList<String> values = new ArrayList<>();
        this.extract(list, values, regex);

        Collections.sort(values);

        for (int i = 0; i < values.size()-1; i++) {
            String value = values.get(i + 1);
            if (values.get(i).equals(value)) {
                throw new WebConfigDuplicatedException(StrUtil.format(errMsg, value));
            }
        }

    }

    private void extract(List<Element> list,List<String> values, String tag) {
        for (Element element : list) {
            List<Element> tags = element.elements(tag);
            String value;
            if (tags.size() == 1) {
                value = element.elementText(tag);
                values.add(value);
                return;
            }
            for (Element e : tags) {
                value = e.getText();
                values.add(value);
            }
        }
    }

    private void extract(List<Element> list, Map<String, String> map, String key, String value) {
        for (Element element : list) {
            String k = element.elementText(key);
            String v = element.elementText(value);
            if (v == null || v.equals("")) {
                String servletName = this.url_servletName.get(k);
                v = this.servletName_className.get(servletName);
            }
            map.put(k,v);
        }
    }

    private void extractFromFilterTag(List<Element> list, Map<String, String> map, String key, String value) {
        for (Element element : list) {
            String k = element.elementText(key);
            String v = element.elementText(value);
            map.put(k,v);
        }
    }

    private void extractFromFilterMapping(List<Element> list, Map<String, List<String>> map, String key, String value) {
        Set<String> tempKeySet = new HashSet<>();
        for (Element element : list) {
            String k = element.elementText(key);
            String v = element.elementText(value);
            if (v == null || v.equals("")) {
                List<String> names = this.url_FilterNames.get(k);
                List<String> classNames = new ArrayList<>();
                for (String name : names) {
                    String className = this.filterName_className.get(name);
                    classNames.add(className);
                }
                map.put(k, classNames);
            } else {
                if (tempKeySet.contains(k)) {
                    map.get(k).add(v);
                } else {
                    List<String> values = new ArrayList<>();
                    values.add(v);
                    map.put(k,values);
                }
                tempKeySet.add(k);
            }
        }
    }

    public Element parseWebXml() {
        Element element = null;
        try {
            Document doc = new SAXReader().read(this.contextWebXmlFile);
            element = doc.getRootElement();
        } catch (DocumentException e) {
            throw new RuntimeException(e);
        }
        return element;
    }

    public String getServletClassName(String uri) {
        return this.url_servletClassName.get(uri);
    }

    public String getPath() {
        return this.path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public String getBase() {
        return this.base;
    }

    public void setBase(String base) {
        this.base = base;
    }

    public File getContextWebXmlFile() {
        return this.contextWebXmlFile;
    }

    public void setHost(Host host) {
        this.host = host;
    }

    public void setReloadable(boolean reloadable) {
        this.reloadable = reloadable;
    }

    public boolean isReloadable() {
        return this.reloadable;
    }

    public WebappClassLoader getWebappClassLoader() {
        return this.webappClassLoader;
    }

    public ServletContext getServletContext() {
        return this.servletContext;
    }

    public Enumeration<String> getServletNames() {
        return Collections.enumeration(this.servletName_className.keySet());
    }

    public synchronized HttpServlet getServlet(Class aClass) throws IllegalAccessException, InstantiationException, ServletException {
        HttpServlet servlet = this.servletPool.get(aClass);
        if (servlet == null) {
            servlet = (HttpServlet)aClass.newInstance();

            String className = aClass.getName();
            String servletName = this.className_servletName.get(className);
            Map<String, String> initParameters = this.servletClassName_initParams.get(className);

            // ServletContext来自ApplicationContext，本身就包含了Context，
            // 其实ServletContext代表了整个web应用，因为web应用中的servlet之间的通讯需要用到，
            // 所以将ServletContext设置到servletConfig当中。
            ServletConfig servletConfig = new StandardServletConfig(this.servletContext, initParameters, servletName);
            servlet.init(servletConfig);

            this.servletPool.put(aClass, servlet);
        }
        return servlet;
    }

    private void destroyServlets() {
        Collection<HttpServlet> servlets = this.servletPool.values();
        for (HttpServlet servlet : servlets) {
            servlet.destroy();
        }
    }

    public void reload() {
        this.host.reload(this);
    }

    public void stop() {
        try {
            this.triggerListener("stop");
            this.webappClassLoader.close();
            this.contextFileChangeWatcher.stop();
            this.destroyServlets();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
